package org.dromara.common.mongo.wrapper;

import com.mongodb.BasicDBList;
import org.bson.BsonRegularExpression;
import org.bson.BsonType;
import org.bson.Document;
import org.bson.types.Binary;
import org.dromara.common.mongo.lambda.ReflectionUtil;
import org.dromara.common.mongo.lambda.SerializableFunction;
import org.springframework.data.domain.Example;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Point;
import org.springframework.data.geo.Shape;
import org.springframework.data.mongodb.InvalidMongoDbApiUsageException;
import org.springframework.data.mongodb.core.geo.GeoJson;
import org.springframework.data.mongodb.core.geo.Sphere;
import org.springframework.data.mongodb.core.query.*;
import org.springframework.data.mongodb.core.schema.JsonSchemaObject;
import org.springframework.data.mongodb.core.schema.JsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
import org.springframework.data.mongodb.util.RegexFlags;
import org.springframework.lang.Nullable;
import org.springframework.util.*;

import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static org.springframework.util.ObjectUtils.nullSafeHashCode;

/**
 * 用于创建查询的中心类。它遵循流畅的API风格，因此您可以轻松地将多个条件链接在一起。WrapperCriteria的静态导入。其中方法提高了可读性。
 * @Author: Stars
 * @Date: 2023/6/4 22:26
 * @see Criteria
 */
public class WrapperCriteria<T> implements CriteriaDefinition {

    /**
     * Custom "not-null" object as we have to be able to work with {@literal null} values as well.
     */
    private static final Object NOT_SET = new Object();

    private @Nullable String key;
    private List<WrapperCriteria> criteriaChain;
    private LinkedHashMap<String, Object> criteria = new LinkedHashMap<String, Object>();
    private @Nullable Object isValue = NOT_SET;

    public WrapperCriteria() {
        this.criteriaChain = new ArrayList<WrapperCriteria>();
    }

    public static <T, R>  WrapperCriteria<T> newWrapper(){
        return new WrapperCriteria<T>();
    }

    public WrapperCriteria(String key) {
        this.criteriaChain = new ArrayList<WrapperCriteria>();
        this.criteriaChain.add(this);
        this.key = key;
    }

    public WrapperCriteria(SerializableFunction<T,?> function) {
        this.criteriaChain = new ArrayList<WrapperCriteria>();
        this.criteriaChain.add(this);
        this.key = ReflectionUtil.getFieldName(function);
    }

    protected WrapperCriteria(List<WrapperCriteria> criteriaChain, String key) {
        this.criteriaChain = criteriaChain;
        this.criteriaChain.add(this);
        this.key = key;
    }


    /**
     * Static factory method to create a {@link Criteria} matching an example object.
     *
     * @param example must not be {@literal null}.
     * @return new instance of {@link Criteria}.
     * @see Criteria#alike(Example)
     * @since 1.8
     */
    public static WrapperCriteria byExample(Object example) {
        return byExample(Example.of(example));
    }

    /**
     * Static factory method to create a {@link Criteria} matching an example object. <br />
     * By default the {@link Example} uses typed matching restricting it to probe assignable types. For example, when
     * sticking with the default type key ({@code _class}), the query has restrictions such as
     * <code>_class : &#123; $in : [com.acme.Person] &#125; </code>. <br />
     * To avoid the above mentioned type restriction use an {@link UntypedExampleMatcher} with
     * {@link Example#of(Object, org.springframework.data.domain.ExampleMatcher)}.
     *
     * @param example must not be {@literal null}.
     * @return new instance of {@link Criteria}.
     * @see Criteria#alike(Example)
     * @since 1.8
     */
    public static WrapperCriteria byExample(Example<?> example) {
        return new WrapperCriteria().alike(example);
    }

    /**
     * Static factory method to create a {@link Criteria} matching documents against a given structure defined by the
     * {@link MongoJsonSchema} using ({@code $jsonSchema}) operator.
     *
     * @param schema must not be {@literal null}.
     * @return this
     * @since 2.1
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/jsonSchema/">MongoDB Query operator:
     *      $jsonSchema</a>
     */
    public static WrapperCriteria matchingDocumentStructure(MongoJsonSchema schema) {
        return new WrapperCriteria().andDocumentStructureMatches(schema);
    }

    /**
     * Static factory method to create a Criteria using the provided key
     *
     * @return new instance of {@link Criteria}.
     */
    public WrapperCriteria<T> and(SerializableFunction<T,?> function) {
        return new WrapperCriteria(this.criteriaChain, ReflectionUtil.getFieldName(function));
    }
    public WrapperCriteria<T> and(String key) {
        return new WrapperCriteria(this.criteriaChain, key);
    }

    /**
     * Creates a criterion using equality
     *
     * @param value can be {@literal null}.
     * @return this.
     */
    public WrapperCriteria<T> is(@Nullable Object value) {

        if (!isValue.equals(NOT_SET)) {
            throw new InvalidMongoDbApiUsageException(
                    "Multiple 'is' values declared. You need to use 'and' with multiple criteria");
        }

        if (lastOperatorWasNot()) {
            throw new InvalidMongoDbApiUsageException("Invalid query: 'not' can't be used with 'is' - use 'ne' instead.");
        }

        this.isValue = value;
        return this;
    }

    /**
     * Creates a criterion using {@literal null} equality comparison which matches documents that either contain the item
     * field whose value is {@literal null} or that do not contain the item field.
     * <br />
     * Use {@link #isNullValue()} to only query for documents that contain the field whose value is equal to
     * {@link BsonType#NULL}. <br />
     * Use {@link #exists(boolean)} to query for documents that do (not) contain the field.
     *
     * @return this.
     * @see <a href="https://docs.mongodb.com/manual/tutorial/query-for-null-fields/#equality-filter">Query for Null or
     *      Missing Fields: Equality Filter</a>
     * @since 3.3
     */
    public WrapperCriteria isNull() {
        return is(null);
    }

    /**
     * Creates a criterion using a {@link BsonType} comparison which matches only documents that contain the item
     * field whose value is equal to {@link BsonType#NULL}.
     * <br />
     * Use {@link #isNull()} to query for documents that contain the field with a {@literal null} value or do not contain the
     * field at all. <br />
     * Use {@link #exists(boolean)} to query for documents that do (not) contain the field.
     *
     * @return this.
     * @see <a href="https://docs.mongodb.com/manual/tutorial/query-for-null-fields/#type-check">Query for Null or Missing
     *      Fields: Type Check</a>
     * @since 3.3
     */
    public WrapperCriteria isNullValue() {

        criteria.put("$type", BsonType.NULL.getValue());
        return this;
    }

    private boolean lastOperatorWasNot() {
        return !this.criteria.isEmpty() && "$not".equals(this.criteria.keySet().toArray()[this.criteria.size() - 1]);
    }

    /**
     * Creates a criterion using the {@literal $ne} operator.
     *
     * @param value can be {@literal null}.
     * @return this.
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/ne/">MongoDB Query operator: $ne</a>
     */
    public WrapperCriteria<T> ne(@Nullable Object value) {
        criteria.put("$ne", value);
        return this;
    }

    /**
     * Creates a criterion using the {@literal $lt} operator.
     *
     * @param value must not be {@literal null}.
     * @return this.
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/lt/">MongoDB Query operator: $lt</a>
     */
    public WrapperCriteria<T> lt(Object value) {
        criteria.put("$lt", value);
        return this;
    }

    /**
     * Creates a criterion using the {@literal $lte} operator.
     *
     * @param value must not be {@literal null}.
     * @return this.
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/lte/">MongoDB Query operator: $lte</a>
     */
    public WrapperCriteria<T> lte(Object value) {
        criteria.put("$lte", value);
        return this;
    }

    /**
     * Creates a criterion using the {@literal $gt} operator.
     *
     * @param value must not be {@literal null}.
     * @return this.
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/gt/">MongoDB Query operator: $gt</a>
     */
    public WrapperCriteria gt(Object value) {
        criteria.put("$gt", value);
        return this;
    }

    /**
     * Creates a criterion using the {@literal $gte} operator.
     *
     * @param value can be {@literal null}.
     * @return this.
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/gte/">MongoDB Query operator: $gte</a>
     */
    public WrapperCriteria<T> gte(Object value) {
        criteria.put("$gte", value);
        return this;
    }

    /**
     * Creates a criterion using the {@literal $in} operator.
     *
     * @param values the values to match against
     * @return this.
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/in/">MongoDB Query operator: $in</a>
     */
    public WrapperCriteria<T> in(Object... values) {
        if (values.length > 1 && values[1] instanceof Collection) {
            throw new InvalidMongoDbApiUsageException(
                    "You can only pass in one argument of type " + values[1].getClass().getName());
        }
        criteria.put("$in", Arrays.asList(values));
        return this;
    }

    /**
     * Creates a criterion using the {@literal $in} operator.
     *
     * @param values the collection containing the values to match against
     * @return this.
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/in/">MongoDB Query operator: $in</a>
     */
    public WrapperCriteria<T> in(Collection<?> values) {
        criteria.put("$in", values);
        return this;
    }

    /**
     * Creates a criterion using the {@literal $nin} operator.
     *
     * @param values
     * @return this.
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/nin/">MongoDB Query operator: $nin</a>
     */
    public WrapperCriteria<T> nin(Object... values) {
        return nin(Arrays.asList(values));
    }

    /**
     * Creates a criterion using the {@literal $nin} operator.
     *
     * @param values must not be {@literal null}.
     * @return this.
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/nin/">MongoDB Query operator: $nin</a>
     */
    public WrapperCriteria<T> nin(Collection<?> values) {
        criteria.put("$nin", values);
        return this;
    }

    /**
     * Creates a criterion using the {@literal $mod} operator.
     *
     * @param value must not be {@literal null}.
     * @param remainder must not be {@literal null}.
     * @return this.
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/mod/">MongoDB Query operator: $mod</a>
     */
    public WrapperCriteria<T> mod(Number value, Number remainder) {
        List<Object> l = new ArrayList<Object>();
        l.add(value);
        l.add(remainder);
        criteria.put("$mod", l);
        return this;
    }

    /**
     * Creates a criterion using the {@literal $all} operator.
     *
     * @param values must not be {@literal null}.
     * @return this.
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/all/">MongoDB Query operator: $all</a>
     */
    public WrapperCriteria<T> all(Object... values) {
        return all(Arrays.asList(values));
    }

    /**
     * Creates a criterion using the {@literal $all} operator.
     *
     * @param values must not be {@literal null}.
     * @return this.
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/all/">MongoDB Query operator: $all</a>
     */
    public WrapperCriteria<T> all(Collection<?> values) {
        criteria.put("$all", values);
        return this;
    }

    /**
     * Creates a criterion using the {@literal $size} operator.
     *
     * @param size
     * @return this.
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/size/">MongoDB Query operator: $size</a>
     */
    public WrapperCriteria<T> size(int size) {
        criteria.put("$size", size);
        return this;
    }

    /**
     * Creates a criterion using the {@literal $exists} operator.
     *
     * @param value
     * @return this.
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/exists/">MongoDB Query operator: $exists</a>
     */
    public WrapperCriteria<T> exists(boolean value) {
        criteria.put("$exists", value);
        return this;
    }

    /**
     * Creates a criterion using the {@literal $sampleRate} operator.
     *
     * @param sampleRate sample rate to determine number of documents to be randomly selected from the input. Must be
     *          between {@code 0} and {@code 1}.
     * @return this.
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/aggregation/sampleRate/">MongoDB Query operator:
     *      $sampleRate</a>
     * @since 3.3
     */
    public WrapperCriteria<T> sampleRate(double sampleRate) {

        Assert.isTrue(sampleRate >= 0, "The sample rate must be greater than zero!");
        Assert.isTrue(sampleRate <= 1, "The sample rate must not be greater than one!");

        criteria.put("$sampleRate", sampleRate);
        return this;
    }

    /**
     * Creates a criterion using the {@literal $type} operator.
     *
     * @param typeNumber
     * @return this.
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/type/">MongoDB Query operator: $type</a>
     */
    public WrapperCriteria<T> type(int typeNumber) {
        criteria.put("$type", typeNumber);
        return this;
    }

    /**
     * Creates a criterion using the {@literal $type} operator.
     *
     * @param types must not be {@literal null}.
     * @return this.
     * @since 2.1
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/type/">MongoDB Query operator: $type</a>
     */
    public WrapperCriteria<T> type(JsonSchemaObject.Type... types) {

        Assert.notNull(types, "Types must not be null!");
        Assert.noNullElements(types, "Types must not contain null.");

        return type(Arrays.asList(types));
    }

    /**
     * Creates a criterion using the {@literal $type} operator.
     *
     * @param types must not be {@literal null}.
     * @return this.
     * @since 3.2
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/type/">MongoDB Query operator: $type</a>
     */
    public WrapperCriteria<T> type(Collection<JsonSchemaObject.Type> types) {

        Assert.notNull(types, "Types must not be null!");

        criteria.put("$type", types.stream().map(JsonSchemaObject.Type::value).collect(Collectors.toList()));
        return this;
    }

    /**
     * Creates a criterion using the {@literal $not} meta operator which affects the clause directly following
     *
     * @return this.
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/not/">MongoDB Query operator: $not</a>
     */
    public WrapperCriteria<T> not() {
        return not(null);
    }

    /**
     * Creates a criterion using the {@literal $not} operator.
     *
     * @param value can be {@literal null}.
     * @return this.
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/not/">MongoDB Query operator: $not</a>
     */
    private WrapperCriteria<T> not(@Nullable Object value) {
        criteria.put("$not", value);
        return this;
    }

    /**
     * Creates a criterion using a {@literal $regex} operator.
     *
     * @param regex must not be {@literal null}.
     * @return this.
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/regex/">MongoDB Query operator: $regex</a>
     */
    public WrapperCriteria<T> regex(String regex) {
        return regex(regex, null);
    }

    /**
     * 前后模糊查询
     * @param regex
     * @return
     */
    public WrapperCriteria<T> like(String regex) {
        return regex(Pattern.compile("^.*" + regex + ".*$"));
    }

    /**
     * Creates a criterion using a {@literal $regex} and {@literal $options} operator.
     *
     * @param regex must not be {@literal null}.
     * @param options can be {@literal null}.
     * @return this.
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/regex/">MongoDB Query operator: $regex</a>
     */
    public WrapperCriteria<T> regex(String regex, @Nullable String options) {
        return regex(toPattern(regex, options));
    }

    /**
     * Syntactical sugar for {@link #is(Object)} making obvious that we create a regex predicate.
     *
     * @param pattern must not be {@literal null}.
     * @return this.
     */
    public WrapperCriteria<T> regex(Pattern pattern) {

        Assert.notNull(pattern, "Pattern must not be null!");

        if (lastOperatorWasNot()) {
            return not(pattern);
        }

        this.isValue = pattern;
        return this;
    }

    /**
     * Use a MongoDB native {@link BsonRegularExpression}.
     *
     * @param regex must not be {@literal null}.
     * @return this.
     */
    public WrapperCriteria<T> regex(BsonRegularExpression regex) {

        if (lastOperatorWasNot()) {
            return not(regex);
        }

        this.isValue = regex;
        return this;
    }

    private Pattern toPattern(String regex, @Nullable String options) {

        Assert.notNull(regex, "Regex string must not be null!");

        return Pattern.compile(regex, RegexFlags.toRegexFlags(options));
    }

    /**
     * Creates a geospatial criterion using a {@literal $geoWithin $centerSphere} operation. This is only available for
     * Mongo 2.4 and higher.
     *
     * @param circle must not be {@literal null}
     * @return this.
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/geoWithin/">MongoDB Query operator:
     *      $geoWithin</a>
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/centerSphere/">MongoDB Query operator:
     *      $centerSphere</a>
     */
    public WrapperCriteria withinSphere(Circle circle) {

        Assert.notNull(circle, "Circle must not be null!");

        criteria.put("$geoWithin", new GeoCommand(new Sphere(circle)));
        return this;
    }

    /**
     * Creates a geospatial criterion using a {@literal $geoWithin} operation.
     *
     * @param shape must not be {@literal null}.
     * @return this.
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/geoWithin/">MongoDB Query operator:
     *      $geoWithin</a>
     */
    public WrapperCriteria within(Shape shape) {

        Assert.notNull(shape, "Shape must not be null!");

        criteria.put("$geoWithin", new GeoCommand(shape));
        return this;
    }

    /**
     * Creates a geospatial criterion using a {@literal $near} operation.
     *
     * @param point must not be {@literal null}
     * @return this.
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/near/">MongoDB Query operator: $near</a>
     */
    public WrapperCriteria<T> near(Point point) {

        Assert.notNull(point, "Point must not be null!");

        criteria.put("$near", point);
        return this;
    }

    /**
     * Creates a geospatial criterion using a {@literal $nearSphere} operation. This is only available for Mongo 1.7 and
     * higher.
     *
     * @param point must not be {@literal null}
     * @return this.
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/nearSphere/">MongoDB Query operator:
     *      $nearSphere</a>
     */
    public WrapperCriteria<T> nearSphere(Point point) {

        Assert.notNull(point, "Point must not be null!");

        criteria.put("$nearSphere", point);
        return this;
    }

    /**
     * Creates criterion using {@code $geoIntersects} operator which matches intersections of the given {@code geoJson}
     * structure and the documents one. Requires MongoDB 2.4 or better.
     *
     * @param geoJson must not be {@literal null}.
     * @return this.
     * @since 1.8
     */
    @SuppressWarnings("rawtypes")
    public WrapperCriteria intersects(GeoJson geoJson) {

        Assert.notNull(geoJson, "GeoJson must not be null!");
        criteria.put("$geoIntersects", geoJson);
        return this;
    }

    /**
     * Creates a geo-spatial criterion using a {@literal $maxDistance} operation, for use with $near
     *
     * @param maxDistance
     * @return this.
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/maxDistance/">MongoDB Query operator:
     *      $maxDistance</a>
     */
    public WrapperCriteria maxDistance(double maxDistance) {

        if (createNearCriteriaForCommand("$near", "$maxDistance", maxDistance)
                || createNearCriteriaForCommand("$nearSphere", "$maxDistance", maxDistance)) {
            return this;
        }

        criteria.put("$maxDistance", maxDistance);
        return this;
    }

    /**
     * Creates a geospatial criterion using a {@literal $minDistance} operation, for use with {@literal $near} or
     * {@literal $nearSphere}.
     *
     * @param minDistance
     * @return this.
     * @since 1.7
     */
    public WrapperCriteria minDistance(double minDistance) {

        if (createNearCriteriaForCommand("$near", "$minDistance", minDistance)
                || createNearCriteriaForCommand("$nearSphere", "$minDistance", minDistance)) {
            return this;
        }

        criteria.put("$minDistance", minDistance);
        return this;
    }

    /**
     * Creates a criterion using the {@literal $elemMatch} operator
     *
     * @param criteria must not be {@literal null}.
     * @return this.
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/elemMatch/">MongoDB Query operator:
     *      $elemMatch</a>
     */
    public WrapperCriteria elemMatch(Criteria criteria) {
        this.criteria.put("$elemMatch", criteria.getCriteriaObject());
        return this;
    }

    /**
     * Creates a criterion using the given object as a pattern.
     *
     * @param sample must not be {@literal null}.
     * @return this.
     * @since 1.8
     */
    public WrapperCriteria alike(Example<?> sample) {

        if (StringUtils.hasText(this.getKey())) {

            criteria.put("$example", sample);
            return this;
        }

        WrapperCriteria exampleCriteria = new WrapperCriteria();
        exampleCriteria.criteria.put("$example", sample);
        return registerCriteriaChainElement(exampleCriteria);
    }

    /**
     * Creates a criterion ({@code $jsonSchema}) matching documents against a given structure defined by the
     * {@link MongoJsonSchema}. <br />
     * <strong>NOTE:</strong> {@code $jsonSchema} cannot be used on field/property level but defines the whole document
     * structure. Please use
     * {@link MongoJsonSchema.MongoJsonSchemaBuilder#properties(JsonSchemaProperty...)}
     * to specify nested fields or query them using the {@link #type(JsonSchemaObject.Type...) $type} operator.
     *
     * @param schema must not be {@literal null}.
     * @return this
     * @since 2.1
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/jsonSchema/">MongoDB Query operator:
     *      $jsonSchema</a>
     */
    public WrapperCriteria andDocumentStructureMatches(MongoJsonSchema schema) {

        Assert.notNull(schema, "Schema must not be null!");

        WrapperCriteria schemaCriteria = new WrapperCriteria();
        schemaCriteria.criteria.putAll(schema.toDocument());

        return registerCriteriaChainElement(schemaCriteria);
    }

    /**
     * Use {@link Criteria.BitwiseCriteriaOperators} as gateway to create a criterion using one of the
     * <a href="https://docs.mongodb.com/manual/reference/operator/query-bitwise/">bitwise operators</a> like
     * {@code $bitsAllClear}.
     *
     * @return new instance of {@link Criteria.BitwiseCriteriaOperators}. Never {@literal null}.
     * @since 2.1
     */
    public BitwiseCriteriaOperators bits() {
        return new BitwiseCriteriaOperatorsImpl(this);
    }

    /**
     * Creates a criteria using the {@code $or} operator for all of the provided criteria.
     * <p>
     * Note that MongoDB doesn't support an {@code $nor} operator to be wrapped in a {@code $not} operator.
     *
     * @throws IllegalArgumentException if this method follows a {@link #not()} call directly.
     * @param criteria must not be {@literal null}.
     * @return this.
     */
    public WrapperCriteria orOperator(WrapperCriteria... criteria) {

        Assert.notNull(criteria, "Criteria must not be null!");

        return orOperator(Arrays.asList(criteria));
    }

    /**
     * Creates a criteria using the {@code $or} operator for all of the provided criteria.
     * <p>
     * Note that MongoDB doesn't support an {@code $nor} operator to be wrapped in a {@code $not} operator.
     *
     * @throws IllegalArgumentException if this method follows a {@link #not()} call directly.
     * @param criteria must not be {@literal null}.
     * @return this.
     * @since 3.2
     */
    public WrapperCriteria orOperator(Collection<WrapperCriteria> criteria) {

        Assert.notNull(criteria, "Criteria must not be null!");

        BasicDBList bsonList = createCriteriaList(criteria);
        return registerCriteriaChainElement(new WrapperCriteria("$or").is(bsonList));
    }

    /**
     * Creates a criteria using the {@code $nor} operator for all of the provided criteria.
     * <p>
     * Note that MongoDB doesn't support an {@code $nor} operator to be wrapped in a {@code $not} operator.
     *
     * @throws IllegalArgumentException if this method follows a {@link #not()} call directly.
     * @param criteria must not be {@literal null}.
     * @return this.
     */
    public WrapperCriteria norOperator(WrapperCriteria... criteria) {

        Assert.notNull(criteria, "Criteria must not be null!");

        return norOperator(Arrays.asList(criteria));
    }

    /**
     * Creates a criteria using the {@code $nor} operator for all of the provided criteria.
     * <p>
     * Note that MongoDB doesn't support an {@code $nor} operator to be wrapped in a {@code $not} operator.
     *
     * @throws IllegalArgumentException if this method follows a {@link #not()} call directly.
     * @param criteria must not be {@literal null}.
     * @return this.
     * @since 3.2
     */
    public WrapperCriteria norOperator(Collection<WrapperCriteria> criteria) {

        Assert.notNull(criteria, "Criteria must not be null!");

        BasicDBList bsonList = createCriteriaList(criteria);
        return registerCriteriaChainElement(new WrapperCriteria("$nor").is(bsonList));
    }

    /**
     * Creates a criteria using the {@code $and} operator for all of the provided criteria.
     * <p>
     * Note that MongoDB doesn't support an {@code $and} operator to be wrapped in a {@code $not} operator.
     *
     * @throws IllegalArgumentException if this method follows a {@link #not()} call directly.
     * @param criteria must not be {@literal null}.
     * @return this.
     */
    public WrapperCriteria andOperator(WrapperCriteria... criteria) {

        Assert.notNull(criteria, "Criteria must not be null!");

        return andOperator(Arrays.asList(criteria));
    }

    /**
     * Creates a criteria using the {@code $and} operator for all of the provided criteria.
     * <p>
     * Note that MongoDB doesn't support an {@code $and} operator to be wrapped in a {@code $not} operator.
     *
     * @throws IllegalArgumentException if this method follows a {@link #not()} call directly.
     * @param criteria must not be {@literal null}.
     * @return this.
     * @since 3.2
     */
    public WrapperCriteria andOperator(Collection<WrapperCriteria> criteria) {

        Assert.notNull(criteria, "Criteria must not be null!");

        BasicDBList bsonList = createCriteriaList(criteria);
        return registerCriteriaChainElement(new WrapperCriteria("$and").is(bsonList));
    }

    private WrapperCriteria registerCriteriaChainElement(WrapperCriteria criteria) {

        if (lastOperatorWasNot()) {
            throw new IllegalArgumentException(
                    "operator $not is not allowed around criteria chain element: " + criteria.getCriteriaObject());
        } else {
            criteriaChain.add(criteria);
        }
        return this;
    }

    /*
     * @see org.springframework.data.mongodb.core.query.CriteriaDefinition#getKey()
     */
    @Override
    @Nullable
    public String getKey() {
        return this.key;
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.mongodb.core.query.CriteriaDefinition#getCriteriaObject()
     */
    public Document getCriteriaObject() {

        if (this.criteriaChain.size() == 1) {
            return criteriaChain.get(0).getSingleCriteriaObject();
        } else if (CollectionUtils.isEmpty(this.criteriaChain) && !CollectionUtils.isEmpty(this.criteria)) {
            return getSingleCriteriaObject();
        } else {
            Document criteriaObject = new Document();
            for (WrapperCriteria c : this.criteriaChain) {
                Document document = c.getSingleCriteriaObject();
                for (String k : document.keySet()) {
                    setValue(criteriaObject, k, document.get(k));
                }
            }
            return criteriaObject;
        }
    }

    protected Document getSingleCriteriaObject() {

        Document document = new Document();
        boolean not = false;

        for (Map.Entry<String, Object> entry : criteria.entrySet()) {

            String key = entry.getKey();
            Object value = entry.getValue();

            if (requiresGeoJsonFormat(value)) {
                value = new Document("$geometry", value);
            }

            if (not) {
                Document notDocument = new Document();
                notDocument.put(key, value);
                document.put("$not", notDocument);
                not = false;
            } else {
                if ("$not".equals(key) && value == null) {
                    not = true;
                } else {
                    document.put(key, value);
                }
            }
        }

        if (!StringUtils.hasText(this.key)) {
            if (not) {
                return new Document("$not", document);
            }
            return document;
        }

        Document queryCriteria = new Document();

        if (!NOT_SET.equals(isValue)) {
            queryCriteria.put(this.key, this.isValue);
            queryCriteria.putAll(document);
        } else {
            queryCriteria.put(this.key, document);
        }

        return queryCriteria;
    }

    private BasicDBList createCriteriaList(Collection<WrapperCriteria> criteria) {
        BasicDBList bsonList = new BasicDBList();
        for (WrapperCriteria c : criteria) {
            bsonList.add(c.getCriteriaObject());
        }
        return bsonList;
    }

    private void setValue(Document document, String key, Object value) {

        Object existing = document.get(key);

        if (existing == null) {
            document.put(key, value);
        } else {
            throw new InvalidMongoDbApiUsageException("Due to limitations of the org.bson.Document, "
                    + "you can't add a second '" + key + "' expression specified as '" + key + " : " + value + "'. "
                    + "Criteria already contains '" + key + " : " + existing + "'.");
        }
    }

    private boolean createNearCriteriaForCommand(String command, String operation, double maxDistance) {

        if (!criteria.containsKey(command)) {
            return false;
        }

        Object existingNearOperationValue = criteria.get(command);

        if (existingNearOperationValue instanceof Document) {

            ((Document) existingNearOperationValue).put(operation, maxDistance);

            return true;

        } else if (existingNearOperationValue instanceof GeoJson) {

            Document dbo = new Document("$geometry", existingNearOperationValue).append(operation, maxDistance);
            criteria.put(command, dbo);

            return true;
        }

        return false;
    }

//    /*
//     * (non-Javadoc)
//     * @see java.lang.Object#equals(java.lang.Object)
//     */
//    @Override
//    public boolean equals(Object obj) {
//
//        if (this == obj) {
//            return true;
//        }
//
//        if (obj == null || !getClass().equals(obj.getClass())) {
//            return false;
//        }
//
//        WrapperCriteria that = (WrapperCriteria) obj;
//
//        if (this.criteriaChain.size() != that.criteriaChain.size()) {
//            return false;
//        }
//
//        for (int i = 0; i < this.criteriaChain.size(); i++) {
//
//            WrapperCriteria left = this.criteriaChain.get(i);
//            WrapperCriteria right = that.criteriaChain.get(i);
//
//            if (!simpleCriteriaEquals(left, right)) {
//                return false;
//            }
//        }
//
//        return true;
//    }

    private boolean simpleCriteriaEquals(WrapperCriteria left, WrapperCriteria right) {

        boolean keyEqual = left.key == null ? right.key == null : left.key.equals(right.key);
        boolean criteriaEqual = left.criteria.equals(right.criteria);
        boolean valueEqual = isEqual(left.isValue, right.isValue);

        return keyEqual && criteriaEqual && valueEqual;
    }

    /**
     * Checks the given objects for equality. Handles {@link Pattern} and arrays correctly.
     *
     * @param left
     * @param right
     * @return
     */
    private boolean isEqual(@Nullable Object left, @Nullable Object right) {

        if (left == null) {
            return right == null;
        }

        if (left instanceof Pattern) {

            if (!(right instanceof Pattern)) {
                return false;
            }

            Pattern leftPattern = (Pattern) left;
            Pattern rightPattern = (Pattern) right;

            return leftPattern.pattern().equals(rightPattern.pattern()) //
                    && leftPattern.flags() == rightPattern.flags();
        }

        if (left instanceof Document) {

            if (!(right instanceof Document)) {
                return false;
            }

            Document leftDocument = (Document) left;
            Document rightDocument = (Document) right;
            Iterator<Map.Entry<String, Object>> leftIterator = leftDocument.entrySet().iterator();
            Iterator<Map.Entry<String, Object>> rightIterator = rightDocument.entrySet().iterator();

            while (leftIterator.hasNext() && rightIterator.hasNext()) {

                Map.Entry<String, Object> leftEntry = leftIterator.next();
                Map.Entry<String, Object> rightEntry = rightIterator.next();

                if (!isEqual(leftEntry.getKey(), rightEntry.getKey())
                        || !isEqual(leftEntry.getValue(), rightEntry.getValue())) {
                    return false;
                }
            }

            return !leftIterator.hasNext() && !rightIterator.hasNext();
        }

        if (Collection.class.isAssignableFrom(left.getClass())) {

            if (!Collection.class.isAssignableFrom(right.getClass())) {
                return false;
            }

            Collection<?> leftCollection = (Collection<?>) left;
            Collection<?> rightCollection = (Collection<?>) right;
            Iterator<?> leftIterator = leftCollection.iterator();
            Iterator<?> rightIterator = rightCollection.iterator();

            while (leftIterator.hasNext() && rightIterator.hasNext()) {

                if (!isEqual(leftIterator.next(), rightIterator.next())) {
                    return false;
                }
            }

            return !leftIterator.hasNext() && !rightIterator.hasNext();
        }

        return ObjectUtils.nullSafeEquals(left, right);
    }

    /*
     * (non-Javadoc)
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {

        int result = 17;

        result += nullSafeHashCode(key);
        result += criteria.hashCode();
        result += nullSafeHashCode(isValue);

        return result;
    }

    private static boolean requiresGeoJsonFormat(Object value) {
        return value instanceof GeoJson
                || (value instanceof GeoCommand && ((GeoCommand) value).getShape() instanceof GeoJson);
    }

    /**
     * MongoDB specific <a href="https://docs.mongodb.com/manual/reference/operator/query-bitwise/">bitwise query
     * operators</a> like {@code $bitsAllClear, $bitsAllSet,...} for usage with {@link Criteria#bits()} and {@link Query}.
     *
     * @author Christoph Strobl
     * @since 2.1
     * @see <a href=
     *      "https://docs.mongodb.com/manual/reference/operator/query-bitwise/">https://docs.mongodb.com/manual/reference/operator/query-bitwise/</a>
     * @currentRead Beyond the Shadows - Brent Weeks
     */
    public interface BitwiseCriteriaOperators {

        /**
         * Creates a criterion using {@literal $bitsAllClear} matching documents where all given bit positions are clear
         * (i.e. 0).
         *
         * @param numericBitmask non-negative numeric bitmask.
         * @return target {@link WrapperCriteria}.
         * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/bitsAllClear/">MongoDB Query operator:
         *      $bitsAllClear</a>
         * @since 2.1
         */
        WrapperCriteria allClear(int numericBitmask);

        /**
         * Creates a criterion using {@literal $bitsAllClear} matching documents where all given bit positions are clear
         * (i.e. 0).
         *
         * @param bitmask string representation of a bitmask that will be converted to its base64 encoded {@link Binary}
         *          representation. Must not be {@literal null} nor empty.
         * @return target {@link WrapperCriteria}.
         * @throws IllegalArgumentException when bitmask is {@literal null} or empty.
         * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/bitsAllClear/">MongoDB Query operator:
         *      $bitsAllClear</a>
         * @since 2.1
         */
        WrapperCriteria allClear(String bitmask);

        /**
         * Creates a criterion using {@literal $bitsAllClear} matching documents where all given bit positions are clear
         * (i.e. 0).
         *
         * @param positions list of non-negative integer positions. Positions start at 0 from the least significant bit.
         *          Must not be {@literal null} nor contain {@literal null} elements.
         * @return target {@link WrapperCriteria}.
         * @throws IllegalArgumentException when positions is {@literal null} or contains {@literal null} elements.
         * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/bitsAllClear/">MongoDB Query operator:
         *      $bitsAllClear</a>
         * @since 2.1
         */
        WrapperCriteria allClear(List<Integer> positions);

        /**
         * Creates a criterion using {@literal $bitsAllSet} matching documents where all given bit positions are set (i.e.
         * 1).
         *
         * @param numericBitmask non-negative numeric bitmask.
         * @return target {@link WrapperCriteria}.
         * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/bitsAllSet/">MongoDB Query operator:
         *      $bitsAllSet</a>
         * @since 2.1
         */
        WrapperCriteria allSet(int numericBitmask);

        /**
         * Creates a criterion using {@literal $bitsAllSet} matching documents where all given bit positions are set (i.e.
         * 1).
         *
         * @param bitmask string representation of a bitmask that will be converted to its base64 encoded {@link Binary}
         *          representation. Must not be {@literal null} nor empty.
         * @return target {@link WrapperCriteria}.
         * @throws IllegalArgumentException when bitmask is {@literal null} or empty.
         * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/bitsAllSet/">MongoDB Query operator:
         *      $bitsAllSet</a>
         * @since 2.1
         */
        WrapperCriteria allSet(String bitmask);

        /**
         * Creates a criterion using {@literal $bitsAllSet} matching documents where all given bit positions are set (i.e.
         * 1).
         *
         * @param positions list of non-negative integer positions. Positions start at 0 from the least significant bit.
         *          Must not be {@literal null} nor contain {@literal null} elements.
         * @return target {@link WrapperCriteria}.
         * @throws IllegalArgumentException when positions is {@literal null} or contains {@literal null} elements.
         * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/bitsAllSet/">MongoDB Query operator:
         *      $bitsAllSet</a>
         * @since 2.1
         */
        WrapperCriteria allSet(List<Integer> positions);

        /**
         * Creates a criterion using {@literal $bitsAllClear} matching documents where any given bit positions are clear
         * (i.e. 0).
         *
         * @param numericBitmask non-negative numeric bitmask.
         * @return target {@link WrapperCriteria}.
         * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/bitsAnyClear/">MongoDB Query operator:
         *      $bitsAnyClear</a>
         * @since 2.1
         */
        WrapperCriteria anyClear(int numericBitmask);

        /**
         * Creates a criterion using {@literal $bitsAllClear} matching documents where any given bit positions are clear
         * (i.e. 0).
         *
         * @param bitmask string representation of a bitmask that will be converted to its base64 encoded {@link Binary}
         *          representation. Must not be {@literal null} nor empty.
         * @return target {@link WrapperCriteria}.
         * @throws IllegalArgumentException when bitmask is {@literal null} or empty.
         * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/bitsAnyClear/">MongoDB Query operator:
         *      $bitsAnyClear</a>
         * @since 2.1
         */
        WrapperCriteria anyClear(String bitmask);

        /**
         * Creates a criterion using {@literal $bitsAllClear} matching documents where any given bit positions are clear
         * (i.e. 0).
         *
         * @param positions list of non-negative integer positions. Positions start at 0 from the least significant bit.
         *          Must not be {@literal null} nor contain {@literal null} elements.
         * @return target {@link WrapperCriteria}.
         * @throws IllegalArgumentException when positions is {@literal null} or contains {@literal null} elements.
         * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/bitsAnyClear/">MongoDB Query operator:
         *      $bitsAnyClear</a>
         * @since 2.1
         */
        WrapperCriteria anyClear(List<Integer> positions);

        /**
         * Creates a criterion using {@literal $bitsAllSet} matching documents where any given bit positions are set (i.e.
         * 1).
         *
         * @param numericBitmask non-negative numeric bitmask.
         * @return target {@link WrapperCriteria}.
         * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/bitsAnySet/">MongoDB Query operator:
         *      $bitsAnySet</a>
         * @since 2.1
         */
        WrapperCriteria anySet(int numericBitmask);

        /**
         * Creates a criterion using {@literal $bitsAnySet} matching documents where any given bit positions are set (i.e.
         * 1).
         *
         * @param bitmask string representation of a bitmask that will be converted to its base64 encoded {@link Binary}
         *          representation. Must not be {@literal null} nor empty.
         * @return target {@link WrapperCriteria}.
         * @throws IllegalArgumentException when bitmask is {@literal null} or empty.
         * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/bitsAnySet/">MongoDB Query operator:
         *      $bitsAnySet</a>
         * @since 2.1
         */
        WrapperCriteria anySet(String bitmask);

        /**
         * Creates a criterion using {@literal $bitsAnySet} matching documents where any given bit positions are set (i.e.
         * 1).
         *
         * @param positions list of non-negative integer positions. Positions start at 0 from the least significant bit.
         *          Must not be {@literal null} nor contain {@literal null} elements.
         * @return target {@link WrapperCriteria}.
         * @throws IllegalArgumentException when positions is {@literal null} or contains {@literal null} elements.
         * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/bitsAnySet/">MongoDB Query operator:
         *      $bitsAnySet</a>
         * @since 2.1
         */
        WrapperCriteria anySet(List<Integer> positions);

    }

    /**
     * Default implementation of {@link BitwiseCriteriaOperators}.
     *
     * @author Christoph Strobl
     * @currentRead Beyond the Shadows - Brent Weeks
     */
    private static class BitwiseCriteriaOperatorsImpl implements BitwiseCriteriaOperators {

        private final WrapperCriteria target;

        BitwiseCriteriaOperatorsImpl(WrapperCriteria target) {
            this.target = target;
        }

        /*
         * (non-Javadoc)
         * @see org.springframework.data.mongodb.core.query.BitwiseCriteriaOperators#allClear(int)
         */
        @Override
        public WrapperCriteria allClear(int numericBitmask) {
            return numericBitmask("$bitsAllClear", numericBitmask);
        }

        /*
         * (non-Javadoc)
         * @see org.springframework.data.mongodb.core.query.BitwiseCriteriaOperators#allClear(java.lang.String)
         */
        @Override
        public WrapperCriteria allClear(String bitmask) {
            return stringBitmask("$bitsAllClear", bitmask);
        }

        /*
         * (non-Javadoc)
         * @see org.springframework.data.mongodb.core.query.BitwiseCriteriaOperators#allClear(java.util.List)
         */
        @Override
        public WrapperCriteria allClear(List<Integer> positions) {
            return positions("$bitsAllClear", positions);
        }

        /*
         * (non-Javadoc)
         * @see org.springframework.data.mongodb.core.query.BitwiseCriteriaOperators#allSet(int)
         */
        @Override
        public WrapperCriteria allSet(int numericBitmask) {
            return numericBitmask("$bitsAllSet", numericBitmask);
        }

        /*
         * (non-Javadoc)
         * @see org.springframework.data.mongodb.core.query.BitwiseCriteriaOperators#allSet(java.lang.String)
         */
        @Override
        public WrapperCriteria allSet(String bitmask) {
            return stringBitmask("$bitsAllSet", bitmask);
        }

        /*
         * (non-Javadoc)
         * @see org.springframework.data.mongodb.core.query.BitwiseCriteriaOperators#allSet(java.util.List)
         */
        @Override
        public WrapperCriteria allSet(List<Integer> positions) {
            return positions("$bitsAllSet", positions);
        }

        /*
         * (non-Javadoc)
         * @see org.springframework.data.mongodb.core.query.BitwiseCriteriaOperators#anyClear(int)
         */
        @Override
        public WrapperCriteria anyClear(int numericBitmask) {
            return numericBitmask("$bitsAnyClear", numericBitmask);
        }

        /*
         * (non-Javadoc)
         * @see org.springframework.data.mongodb.core.query.BitwiseCriteriaOperators#anyClear(java.lang.String)
         */
        @Override
        public WrapperCriteria anyClear(String bitmask) {
            return stringBitmask("$bitsAnyClear", bitmask);
        }

        /*
         * (non-Javadoc)
         * @see org.springframework.data.mongodb.core.query.BitwiseCriteriaOperators#anyClear(java.util.List)
         */
        @Override
        public WrapperCriteria anyClear(List<Integer> positions) {
            return positions("$bitsAnyClear", positions);
        }

        /*
         * (non-Javadoc)
         * @see org.springframework.data.mongodb.core.query.BitwiseCriteriaOperators#anySet(int)
         */
        @Override
        public WrapperCriteria anySet(int numericBitmask) {
            return numericBitmask("$bitsAnySet", numericBitmask);
        }

        /*
         * (non-Javadoc)
         * @see org.springframework.data.mongodb.core.query.BitwiseCriteriaOperators#anySet(java.lang.String)
         */
        @Override
        public WrapperCriteria anySet(String bitmask) {
            return stringBitmask("$bitsAnySet", bitmask);
        }

        /*
         * (non-Javadoc)
         * @see org.springframework.data.mongodb.core.query.BitwiseCriteriaOperators#anySet(java.util.Collection)
         */
        @Override
        public WrapperCriteria anySet(List<Integer> positions) {
            return positions("$bitsAnySet", positions);
        }

        private WrapperCriteria positions(String operator, List<Integer> positions) {

            Assert.notNull(positions, "Positions must not be null!");
            Assert.noNullElements(positions.toArray(), "Positions must not contain null values.");

            target.criteria.put(operator, positions);
            return target;
        }

        private WrapperCriteria stringBitmask(String operator, String bitmask) {

            Assert.hasText(bitmask, "Bitmask must not be null!");

            target.criteria.put(operator, new Binary(Base64Utils.decodeFromString(bitmask)));
            return target;
        }

        private WrapperCriteria numericBitmask(String operator, int bitmask) {

            target.criteria.put(operator, bitmask);
            return target;
        }
    }
}
